/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry;

import static io.sentry.SentryLevel.ERROR;

import io.sentry.hints.Cached;
import io.sentry.hints.Flushable;
import io.sentry.hints.Retryable;
import io.sentry.hints.SubmissionResult;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * DirectoryProcessor
 *
 * @since 2021-06-02
 **/
abstract class DirectoryProcessor {
    private final @NotNull ILogger logger;
    private final long flushTimeoutMillis;

    DirectoryProcessor(final @NotNull ILogger logger, final long flushTimeoutMillis) {
        this.logger = logger;
        this.flushTimeoutMillis = flushTimeoutMillis;
    }

    /**
     * processDirectory
     *
     * @param directory directory
     */
    public void processDirectory(final @NotNull File directory) {
        try {
            logger.log(SentryLevel.DEBUG, "Processing dir. %s", directory.getCanonicalPath());

            if (!directory.exists()) {
                logger.log(
                    SentryLevel.WARNING,
                    "Directory '%s' doesn't exist. No cached events to send.",
                    directory.getCanonicalPath());
                return;
            }
            if (!directory.isDirectory()) {
                logger.log(
                    SentryLevel.ERROR, "Cache dir %s is not a directory.", directory.getCanonicalPath());
                return;
            }

            final File[] listFiles = directory.listFiles();
            if (listFiles == null) {
                logger.log(SentryLevel.ERROR, "Cache dir %s is null.", directory.getCanonicalPath());
                return;
            }

            final File[] filteredListFiles = directory.listFiles((filter, name) -> isRelevantFileName(name));

            logger.log(
                SentryLevel.DEBUG,
                "Processing %d items from cache dir %s",
                filteredListFiles != null ? filteredListFiles.length : 0,
                directory.getCanonicalPath());

            for (File file : listFiles) {
                // it ignores .sentry-native database folder and new ones that might come up
                if (!file.isFile()) {
                    logger.log(SentryLevel.DEBUG, "File %s is not a File.", file.getCanonicalPath());
                    continue;
                }

                logger.log(SentryLevel.DEBUG, "Processing file: %s", file.getCanonicalPath());

                final SendCachedEnvelopeHint hint = new SendCachedEnvelopeHint(flushTimeoutMillis, logger);
                processFile(file, hint);
            }
        } catch (Exception e) {
            try {
                logger.log(SentryLevel.ERROR, e, "Failed processing '%s'", directory.getCanonicalPath());
            } catch (IOException ioException) {
                ioException.getMessage();
            }
        }
    }

    /**
     * processFile
     *
     * @param file file
     * @param hint hint
     */
    protected abstract void processFile(@NotNull File file, @Nullable Object hint);

    /**
     * isRelevantFileName
     * @param fileName fileName
     * @return boolean
     */
    protected abstract boolean isRelevantFileName(String fileName);

    /**
     * SendCachedEnvelopeHint
     *
     * @since 2021-06-02
     **/
    private static final class SendCachedEnvelopeHint
        implements Cached, Retryable, SubmissionResult, Flushable {
        boolean retry = false;
        boolean succeeded = false;

        private final CountDownLatch latch;
        private final long flushTimeoutMillis;
        private final @NotNull ILogger logger;

        SendCachedEnvelopeHint(final long flushTimeoutMillis, final @NotNull ILogger logger) {
            this.flushTimeoutMillis = flushTimeoutMillis;
            this.latch = new CountDownLatch(1);
            this.logger = logger;
        }

        @Override
        public boolean isRetry() {
            return retry;
        }

        @Override
        public void setRetry(boolean retry) {
            this.retry = retry;
        }

        @Override
        public boolean waitFlush() {
            try {
                return latch.await(flushTimeoutMillis, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.log(ERROR, "Exception while awaiting on lock.", e);
            }
            return false;
        }

        @Override
        public void setResult(boolean succeed) {
            this.succeeded = succeed;
            latch.countDown();
        }

        @Override
        public boolean isSuccess() {
            return succeeded;
        }
    }
}
